webpack + React 开发环境

什么是 webpack?

webpack是近期最火的一款模块加载器兼打包工具,它能把各种资源,例如JS(含JSX)、coffee、样式(含less/sass)、图片等都作为模块来使用和处理。
我们可以直接使用 require(XXX) 的形式来引入各模块,即使它们可能需要经过编译(比如JSX和sass),但我们无须在上面花费太多心思,因为 webpack 有着各种健全的加载器(loader)在默默处理这些事情,因为以近期 Github 上各大主流的(React相关)项目来说,它们仓库上所展示的示例已经是基于 webpack 来开发的,比如 React-Bootstrap 和 Redux链接:https://github.com/gaearon/redux
webpack的官网是 http://webpack.github.io/ ,文档地址是 http://webpack.github.io/docs/ ,想对其进行更详细了解的可以点进去瞧一瞧。

webpack 的优势

其优势主要可以归类为如下几个:

  1. webpack 是以 commonJS 的形式来书写脚本,但对 AMD/CMD 的支持也很全面,方便旧项目进行代码迁移。

  2. 能被模块化的不仅仅是 JS 了。

  3. 开发便捷,能替代部分 grunt/gulp 的工作,比如打包、压缩混淆、图片转base64等。

  4. 扩展性强,插件机制完善,特别是支持 React 热插拔(见 react-hot-loader )的功能让人眼前一亮。

我们谈谈第一点。以 AMD/CMD 模式来说,鉴于模块是异步加载的,所以我们常规需要使用 define 函数来帮我们搞回调:

1
2
3
4
5
6
7
8
9
10
define(['package/lib'], function(lib){
function foo(){
lib.log('hello world!');
}
return {
foo: foo
};
});

另外为了可以兼容 commonJS 的写法,我们也可以将 define 这么写:

1
2
3
4
5
6
7
8
9
10
11
12
define(function (require, exports, module){
var someModule = require("someModule");
var anotherModule = require("anotherModule");
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
exports.asplode = function (){
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
};
});

然而对 webpack 来说,我们可以直接在上面书写 commonJS 形式的语法,无须任何 define (毕竟最终模块都打包在一起,webpack 也会最终自动加上自己的加载器):

1
2
3
4
5
6
7
8
9
10
var someModule = require("someModule");
var anotherModule = require("anotherModule");
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
exports.asplode = function (){
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
};

即使你保留了之前 define 的写法也是可以滴,毕竟 webpack 的兼容性相当出色,方便你旧项目的模块直接迁移过来。

安装和配置

一. 安装

我们常规直接使用 npm 的形式来安装:
初始化 npm 环境
首先保证有 node 和 npm 环境,运行node -v和npm -v查看
进入项目目录,运行npm init按照步骤填写最终生成package.json文件,所有使用 npm 做依赖管理的项目,根目录下都会有一个这个文件,该文件描述了项目的基本信息以及一些第三方依赖项(插件)。

1
2
$ npm install webpack webpack-dev-server --save-dev
$ npm i react react-dom --save

安装完成之后,查看package.json可看到多了 devDependencies 和 dependencies 两项,根目录也多了一个node_modules文件夹。

–save 和 –save-dev 的区别

npm i时使用–save和–save-dev,可分别将依赖(插件)记录到package.json中的dependencies和devDependencies下面。
dependencies下记录的是项目在运行时必须依赖的插件,
常见的例如react jquery等,即及时项目打包好了、上线了,这些也是需要用的,否则程序无法正常执行。

devDependencies下记录的是项目在开发过程中使用的插件,
例如这里我们开发过程中需要使用webpack打包,而我在工作中使用fis3打包,但是一旦项目打包发布、上线了之后,webpack和fis3就都没有用了,可卸磨杀驴。

延伸一下,我们的项目有package.json,其他我们用的项目如webpack也有package.json,
见./node_modules/webpack/package.json,其中也有devDependencies和dependencies。
当我们使用npm i webpack时,
./node_modules/webpack/package.json中的dependencies会被 npm 安装上,而devDependencies也没必要安装。

配置 webpack.config.js

为了提高学习效率,webpack最最基础的用法,webpack.config.js这个配置文件详细的讲解一下,

文件格式

webpack.config.js 就是一个普通的 js 文件,符合 commonJS 规范。最后输出一个对象,即module.exports = {…}

输入 & 输出

这个比较基础,不过需要新建./app/index.jsx作为入口文件,目前项目中只有这一个入口文件。不过 webpack 支持多个入口文件,可查阅文档。
输出就是一个bundle.js,js 和 css 都在里面,不过只有在开发环境下才用,发布代码的时候,肯定不能只有这么一个文件,接下来会讲到。

module

针对不同类型的文件,使用不同的loader,当然使用之前要安装,
例如npm i style-loader css-loader –save-dev。该项目代码中,我们用到的文件格式有:js/jsx 代码、css/less 代码、图片、字体文件。
less 是 css 的语法糖,可以更高效低冗余的写 css,不熟悉的朋友可去官网看看,非常简单。
在加载 css/less 时用到了postcss,主要使用autoprefixer功能,帮助自动加 css3 的浏览器前缀,非常好用。
编译 es6 和 jsx 语法时,用到家喻户晓的babel,另外还需增加一个.babelrc的配置文件。

plugins

使用 html 模板(需要npm i html-webpack-plugin –save-dev),这样可以将输出的文件名(如./bundle.js)自动注入到 html 中,不用我们自己手写。手写的话,一旦修改就需要改两个地方。
使用热加载和自动打开浏览器插件

devServer

对 webpack-dev-server 的配置

npm start

在 package.json 中,输入以下代码,将这一串命令封装为npm start,这样就可以运行项目代码了。

1
2
3
"scripts": {
"start": "NODE_ENV=dev webpack-dev-server --progress --colors"
}

代码中NODE_ENV=dev代表当前是开发环境下,这里的”dev”可被 js 代码中的process.env.NODE_ENV得到并做一些其他处理。
定义环境全局变量
以下定义,可使得代码通过DEV得到当前是不是开发模式。

1
2
3
new webpack.DefinePlugin({
__DEV__: JSON.stringify(JSON.parse((process.env.NODE_ENV == 'dev') || 'false'))
})

打开./app/util/localStore.js可以看到if (DEV) { console.error(‘localStorage.getItem报错, ‘, ex.message) },即只有开发环境下才提示error,发布之后就不会提示了。(因为发布的命令中用到NODE_ENV=production)

配置 webpack.production.config.js

开发环境下,可以不用考虑系统的性能,更多考虑的是如何增加开发效率。而发布系统时,就需要考虑发布之后的系统的性能,包括加载速度、缓存等。下面介绍发布用配置代码和开发用的不一样的地方。
发布到 ./build 文件夹中
path: __dirname + “/build”,

vendor

将第三方依赖单独打包。即将 node_modules 文件夹中的代码打包为 vendor.js 将我们自己写的业务代码打包为 app.js。这样有助于缓存,因为在项目维护过程中,第三方依赖不经常变化,而业务代码会经常变化。

md5后缀

为每个打包出来的文件都加md5后缀,例如”/js/[name].[chunkhash:8].js”,可使文件强缓存。
分目录
打包出来的不同类型的文件,放在不同目录下,例如图片文件将放在img/目录下

自动为打包出来的代码增加 copyright 内容

分模块

new webpack.optimize.OccurenceOrderPlugin(),

压缩代码

使用 Uglify 压缩代码,其中warnings: false是去掉代码中的 warning
分离 css 和 js 文件
开发环境下,css 代码是放在整个打包出来的那个 bundle.js 文件中的,发布环境下当然不能混淆在一起,使用new ExtractTextPlugin(‘/css/[name].[chunkhash:8].css’),将 css 代码分离出来。
npm run build
打开package.json,查看以下代码。npm start和npm run build分别是运行代码和打包项目。另外,”start”、”test”可以不用run。

1
2
3
4
"scripts": {
"start": "NODE_ENV=dev webpack-dev-server --progress --colors",
"build": "rm -rf ./build && NODE_ENV=production webpack --config ./webpack.production.config.js --progress --colors"
},

这两个命令主要有以下区别:
前者中默认使用 webpack.config.js 作为配置文件,而后者中强制使用 webpack.production.config.js 作为配置文件
前者NODE_ENV=dev而后者NODE_ENV=production,标识不同的环境。而这个”dev” “production”可以在代码中通过process.env.NODE_ENV获取。
最小化压缩 React
以下配置可以告诉 React 当前是生产环境,请最小化压缩 js ,即把开发环境中的一些提示、警告、判断通通去掉,直流以下发布之后可用的代码。

1
2
3
4
5
new webpack.DefinePlugin({
'process.env':{
'NODE_ENV': JSON.stringify(process.env.NODE_ENV)
}
}),

二. 配置

每个项目下都必须配置有一个 webpack.config.js ,它的作用如同常规的 gulpfile.js/Gruntfile.js ,就是一个配置项,告诉 webpack 它需要做什么。

我们看看下方的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var webpack = require('webpack');
var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js');
module.exports = {
//插件项
plugins: [commonsPlugin],
//页面入口文件配置
entry: {
index : './src/js/page/index.js'
},
//入口文件输出配置
output: {
path: 'dist/js/page',
filename: '[name].js'
},
module: {
//加载器配置
loaders: [
{ test: /\.css$/, loader: 'style-loader!css-loader' },
{ test: /\.js$/, loader: 'jsx-loader?harmony' },
{ test: /\.scss$/, loader: 'style!css!sass?sourceMap'},
{ test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192'}
]
},
//其它解决方案配置
resolve: {
//查找module的话从这里开始查找
root: 'E:/github/flux-example/src', //绝对路径
//自动扩展文件后缀名,意味着我们require模块可以省略不写后缀名
extensions: ['', '.js', '.json', '.scss'],
//模块别名定义,方便后续直接引用别名,无须多写长长的地址
alias: {
AppStore : 'js/stores/AppStores.js',//后续直接 require('AppStore') 即可
ActionType : 'js/actions/ActionType.js',
AppAction : 'js/actions/AppAction.js'
}
}
};

⑴ plugins 是插件项,这里我们使用了一个 CommonsChunkPlugin 的插件,它用于提取多个入口文件的公共脚本部分,然后生成一个 common.js 来方便多页面之间进行复用。

⑵ entry 是页面入口文件配置,output 是对应输出项配置(即入口文件最终要生成什么名字的文件、存放到哪里),其语法大致为:

1
2
3
4
5
6
7
8
9
10
11
{
entry: {
page1: "./page1",
//支持数组形式,将加载数组中的所有模块,但以最后一个模块作为输出
page2: ["./entry1", "./entry2"]
},
output: {
path: "dist/js/page",
filename: "[name].bundle.js"
}
}

该段代码最终会生成一个 page1.bundle.js 和 page2.bundle.js,并存放到 ./dist/js/page 文件夹下。
⑶ module.loaders 是最关键的一块配置。它告知 webpack 每一种文件都需要使用什么加载器来处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
module: {
//加载器配置
loaders: [
//.css 文件使用 style-loader 和 css-loader 来处理
{ test: /\.css$/, loader: 'style-loader!css-loader' },
//.js 文件使用 jsx-loader 来编译处理
{ test: /\.js$/, loader: 'jsx-loader?harmony' },
//.scss 文件使用 style-loader、css-loader 和 sass-loader 来编译处理
{ test: /\.scss$/, loader: 'style!css!sass?sourceMap'},
//图片文件使用 url-loader 来处理,小于8kb的直接转为base64
{ test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192'}
]
}

⑷ 最后是 resolve 配置,这块很好理解,直接写注释了:

1
2
3
4
5
6
7
8
9
10
11
12
resolve: {
//查找module的话从这里开始查找
root: 'E:/github/flux-example/src', //绝对路径
//自动扩展文件后缀名,意味着我们require模块可以省略不写后缀名
extensions: ['', '.js', '.json', '.scss'],
//模块别名定义,方便后续直接引用别名,无须多写长长的地址
alias: {
AppStore : 'js/stores/AppStores.js',//后续直接 require('AppStore') 即可
ActionType : 'js/actions/ActionType.js',
AppAction : 'js/actions/AppAction.js'
}
}

关于 webpack.config.js 更详尽的配置可以参考这里。http://webpack.github.io/docs/configuration.html